// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Provide a class that walks the type heirarchy and pulls inherited docs into
 * sub-types that @override super-type methods.
 */

var {DocMap} = require('rdoc/docmap'),
    {DocInfo} = require('rdoc/docinfo'),
    {Cache} = require('rdoc/util/cache'),
    {Q, Stack} = require('rdoc/util/structs'),

    fs = require('fs'),
    logger = require('ringo/logging').getLogger('Overrides');



/**
 * A class responsible for pulling inherited docs into sub-types that @override
 * super-type methods.
 *
 * @param {DocMap} docMap The DocMap for the given inputs.
 * @param {ConfigParser} config The config options object for this run.
 *
 * @constructor
 */
var Overrides = exports.Overrides = function(docMap, config) {
  /**
   * @type {ConfigParser}
   * @private
   */
  this.config_ = config;

  /**
   * An LRU cache used to store:
   *   1) FunctionDescriptors by class/interface and method name
   *   2) DocInfo objects by path
   *
   * For documenting Closure Library + Relief, the cache grows to about 850
   * entries and all of this seems to fit into memory, so the cache size is
   * set very high.  For larger projects that run into Out of Memory exceptions,
   * a smaller cache size can be specified in the config file.
   *
   * @type {Cache}
   * @private
   */
  this.cache_ = new Cache(config.cacheSize);

  var types = docMap.types,
      classes = Object.create(null),
      processed = Object.create(null),
      subtypes = Object.create(null);

  /*
    For this process "types" means "classes and interfaces".  Typedefs and enums
    are ignored.

    Break all types into two categories:
    - Those that have been processed already, or are known to not need
      processsing.  E.g., interfaces (no point in @override-ing super-interface
      methods), classes with no base type or implemented interfaces.  These
      will be referred to as "processed".

    - Those that have not been processed and might need to be.  E.g., classes
      that have a base type or impelmement 1 or more interfaces.  These will be
      referred to as "subtypes".

    Processing a type involves iterating through all of the type's methods and,
    for any that are flagged with @override, search the super-types (and their
    super-types) for the original method.  Once found, pull any missing doc tags
    down into the type.
  */

  var typeNames = Object.keys(types),
      {INTERFACE, CLASS} = DocMap.Kind;
  for (var i = 0, len = typeNames.length; i < len; ++i) {
    var name = typeNames[i],
        type = types[name];

    if (type.kind === INTERFACE) {
      processed[name] = type;
    }
    else if (type.kind === CLASS) {
      if (type.baseType || (type.implemented && type.implemented.length > 0)) {
        subtypes[name] = type
      }
      else {
        processed[name] = type;
      }
    }
  }

  /**
   * Map of type names to DocMap.Entry records for types that have already
   * been processed or do not require processing: interfaces (no point
   * in @override-ing super-interface methods), classes with no base type or
   * implemented interfaces.
   *
   * @type {Object.<string, DocMap.Entry>}
   * @private
   */
  this.processed_ = processed;

  /**
   * Map of type names to DocMap.entry records for types that may require
   * processing: classes with a base type or implemented interfaces.
   *
   * @type {Object.<string, DocMap.Entry>}
   * @private
   */
  this.subtypes_ = subtypes;

  if (config.verbose) {
    logger.info('# of pre-processed types: ' + Object.keys(processed).length);
    logger.info('# of subtypes: ' + Object.keys(subtypes).length);
  }
};


/**
 * Get a DocInfo object from a source filename.
 *
 * @param {string} filename The source file.
 * @return {DocInfo} The DocInfo object corresponding to the given source file.
 *
 * @private
 */
Overrides.prototype.getDocInfo_ = function(filename) {
  var {docFile} = this.config_.getDocFile(filename);

  var docInfo = this.cache_.get(docFile);
  if (! docInfo) {
    docInfo = DocInfo.fromFile(docFile);
    this.cache_.set(docFile, docInfo);
  }

  return docInfo;
};


/**
 * Process all subtypes.
 */
Overrides.prototype.processAll = function() {
  // Iterate through all subtypes and recursively process their super-types
  // as needed.  By the time we get through all sub-types, they should all
  // be processed.

  var subtypes = this.subtypes_,
      processed = this.processed_,
      typeNames = Object.keys(subtypes);

  logger.info('Starting @override processing.');
  for (var i = 0, len = typeNames.length; i < len; ++i) {
    var name = typeNames[i],
        type = subtypes[name];

    // As we move through the list of type names, entries will be deleted from
    // the "subtypes" map and added to the "processed" map.  The list of type-
    // names, however, is static.  Therefore, types in the typeNames list may
    // be processed by the time iteration gets to them.

    if (type) {
      this.processType_(name, type);
    }
  }
  logger.info('Finished @override processing.');
  
  if (this.config_.verbose) {
    this.cache_.logHits();
  }
};


/**
 * Scan through a type's method definitions looking for any marked @override.
 * For each such method, search through the CLASS heirarchy for 
 *
 * @param {string} name The name of the type being processed.
 * @param {DocMap.Entry} type The class to be processed.
 *
 * @private
 */
Overrides.prototype.processType_ = function(name, type) {
  // Don't do this multiple times for the same type.
  if (this.processed_[name]) {
    return;
  }

  var subtypes = this.subtypes_,
      processed = this.processed_,
      config = this.config_;

  // All interfaces are already considered "processed", since there's no point
  // in a sup-interface @overriding a method from a super-interface.  At this
  // point, make sure the super-class, if any, is processed, then come back.

  var baseType = type.baseType;
  if (subtypes[baseType]) {
    this.processType_(baseType, subtypes[baseType]);
  }

  if (config.verbose) {
    logger.info('Processing type: ' + name);
  }

  var docInfo = this.getDocInfo_(type.filename),
      classDesc = docInfo.classes[name];

  if (! classDesc) {
    throw Error('Type ' + name + ' in DocMap but missing from source file\'s ' +
                'DocInfo file.')
  }
  var methods = classDesc.methods || {},
      methodNames = Object.keys(methods);

  var foundOverrides = false;
  for (var i = 0, len = methodNames.length; i < len; ++i) {
    var methodName = methodNames[i],
        method = methods[methodName];

    if (method.isOverride) {
      foundOverrides = true;

      // Search the super-class and implemented interfaces (recursively)
      // for the original declaration(s) of this method.
      if (config.verbose) {
        logger.info('\tFinding Method: ' + methodName); 
      }
      this.findInSuperTypes_(methodName, method, type);
    }
  }

  if (foundOverrides) {
    // We've made changes to the method descriptors for the type.
    // Re-write the doc file.
    fs.write(config.getDocFile(type.filename).docFile,
             JSON.stringify(docInfo));
  }

  processed[name] = subtypes[name];
  delete subtypes[name];
};


/**
 * Merge two function descriptors.  The first argument is the FD from the
 * sub-type, the second is from a super-type.  If a property is in both the
 * sub- and super-type FDs, the sub-type takes priority EXCEPT for Visibility.
 * For Visibility, stricter visibility always wins.
 *
 * This method modifies the provided "sub" function descriptor, but it DOES NOT
 * touch sub.isOverride or sub.isAbstractMethod.
 *
 * @param {FunctionDescriptor} sub The sub-type function descriptor.
 * @param {functionDescriptor} sup The super-type function descriptor.
 *
 * @private
 */
Overrides.prototype.mergeFDs_ = function(sub, sup) {
  sub.desc = sub.desc || sup.desc;
  sub.deprecationReason = sub.deprecationReason || sup.deprecationReason;

  sub.params = sub.params || sup.params;
  sub.returns = sub.returns || sup.returns;

  var {INHERITED} = DocInfo.Vis,
      subV = sub.visibility || INHERITED,
      supV = sup.visibility || INHERITED;

  // If the super-type's visibility is INHERITED, then leave sub-type's
  // visibility as whatever it already is.
  if (supV !== INHERITED) {
    sub.visibility = subV === INHERITED ? supV : Math.min(subV, supV);
  }
};


/**
 * Search the prototype chain + directly implemented interfaces to find the
 * base method declaration.  When this method is called, all super-classes
 * are guaranteed to have been processed.
 *
 * This method should only be called with a CLASS type.
 *
 * @param {string} name The name of the method.
 * @param {FunctionDescriptor} fd The function descriptor for the method.  This
 *    will be mutated, with the missing pieces being added as they are found in
 *    the super-type-tree.
 * @param {DocMap.Entry} type The type entry, used to determine super-types.
 *
 * @private
 */
Overrides.prototype.findInSuperTypes_ = function(name, fd, type) {
  this.findInSuperClasses_(name, fd, type);
  this.findInImplementedInterfaces_(name, fd, type);
};


/**
 * Search super-classes for @overridden documentation.
 *
 * This method should only be called with a CLASS type.
 *
 * @param {string} name The name of the method.
 * @param {FunctionDescriptor} fd The function descriptor for the method.  This
 *    will be mutated, with the missing pieces being added as they are found in
 *    the super-type-tree.
 * @param {DocMap.Entry} type The type entry, used to determine super-types.
 *
 * @private
 */
Overrides.prototype.findInSuperClasses_ = function(name, fd, type) {
  var processed = this.processed_,
      config = this.config_,
      
      cache = this.cache_,
      superClass = type.baseType;

  var parent = processed[superClass];
  while (parent) {
    if (config.verbose) {
      logger.info('\t\tLooking in super-class: ' + superClass);
    }

    var parentMethod = cache.get(superClass + '#' + name);
    if (! parentMethod) {
      var parentInfo = this.getDocInfo_(parent.filename);

      parentMethod = parentInfo.classes[superClass].methods[name];
    }

    if (parentMethod) {
      this.mergeFDs_(fd, parentMethod);
      cache.set(superClass + '#' + name, parentMethod);
    }

    if (parentMethod && (! parentMethod.isOverride)) {
      // If we found a declaration in a super class AND that method declaration
      // was NOT marked with @override, then we found the original declaration
      // and can stop looking up the chain.
      parent = null;
    }
    else {
      superClass = parent.baseType;
      parent = processed[superClass];
    }
  }
};


/**
 * Search an interface and its super-interfaces for a method descriptor of
 * the given name.
 *
 * @param {string} name The name of the method.
 * @param {FunctionDescriptor} fd The function descriptor for the method.  This
 *    will be mutated, with the missing pieces being added as they are found in
 *    the super-type-tree.
 * @param {DocMap.Entry} type The type entry, used to determine super-types.
 *
 * @private
 */
Overrides.prototype.findInImplementedInterfaces_ = function(name, fd, type) {
  var interfaces = type.implemented;

  // If this type doesn't implement any interfaces, bail out quick.
  if (! (interfaces && interfaces.length > 0)) {
    return;
  }

  // Since super classes should have all the docs for the interfaces that they
  // implement already pulled in, we only need to look in interfaces
  // implemented directly.  In order to support arbitrary interface heirarchies,
  // We need to be able to do a full, breadth-first tree search since any
  // class can implement any number of interfaces, which can themselves extend
  // any number of super-interfaces.
  //
  // In practice, the only interface I've seen that extends another interface
  // is relief.nav.Handler, but we have to be prepared for more complex
  // cases.

  var processed = this.processed_,
      config = this.config_,
      
      cache = this.cache_,
      q = new Q(); // A queue used in the tree search.

  for (var i = 0, len = interfaces.length; i < len; ++i) {
    q.enQ(interfaces[i]);
  }
  
  while (q.length() > 0) {
    var interfName = q.deQ(),
        interf = processed[interfName],
        extended = interf.extendedInterfaces || [];

    for (i = 0, len = extended.length; i < len; ++i) {
      q.enQ(extended[i]);
    }

    var method = cache.get(interfName + '#' + name);
    
    if (! method) {
      var docInfo = this.getDocInfo_(interf.filename),
          methods = docInfo.interfaces[interfName].methods || {};

      method = methods[name];
      cache.set(interfName + '#' + name, method);
    }

    if (method) {
      if (config.verbose) {  
        logger.info('\t\tFound method in interface ' + interfName);
      }
      this.mergeFDs_(fd, method);
    }
  }
};
